﻿using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

/// <Sample usage>
///     * -- For long commands wishing to use threading -- *
///     
///     using (App app = new App())
///     {
///         app.Arguments = "--passphrase-fd 0 --no-verbose --batch --trust-model always --default-recipient-self --armor --encrypt";
///         app.Passphrase = "Not my real passphrase";
///
///         Stream input = App.ConvertTo("Hello there!");
///         Stream output = new MemoryStream();
///         app.Run(input, output);
///
///         response = App.ConvertTo(output);
///     }
///
///     * -- For quick commands -- *
///
///     string response = null;
///     using (App app = new App())
///     {
///         app.Arguments = "--list-keys";
///         int exitCode = app.Run(out response);
///     }
/// </Sample usage>

namespace GnuPGNotepad
{
    public class App : IDisposable
    {
        #region constructor functions
        public App() 
        {
            this.BinaryPath = Config.GPGBinaryPath;
        }
        #endregion // constructor functions

        #region public declarations
        public Settings Config = new Settings();
        public bool IsDisposed { get; private set; }

        /// <summary>
        /// Gets or sets the path to the application binary
        /// </summary>
        public string BinaryPath { get; set; }

        /// <summary>
        /// Gets or sets the working directory
        /// </summary>
        public string WorkingDirectory { get; set; }

        /// <summary>
        /// Gets or sets the command line arguements for the application
        /// </summary>
        public string Arguments { get; set; }

        /// <summary>
        /// Gets the results from the execution of the application
        /// </summary>
        public string Response { get; private set; }

        /// <summary>
        /// Gets the exit code from the execution of the application
        /// </summary>
        public int ExitCode { get; private set; }

        /// <summary>
        /// gets any exit message provided by application at execution
        /// </summary>
        public string ExitMessage { get; private set; }

        public PGPAction Action {
            get { return action; }
            set { action = value; } 
        }

        /// <summary>
        /// Gets or sets the thread buffer size for the application execution
        /// </summary>
        public int StreamBufferSize
        {
            get { return streambuffersize; }
            set { streambuffersize = value; }
        }
        #endregion // public declarations

        #region private declarations
        private Process process;
        private Stream stdOutputStream;
        private Stream stdConsoleStream;
        private int streambuffersize = 4096;    // default stream buffer size (kb)
        private PGPAction action = PGPAction.SignAndEncrypt;

        /// <summary>
        /// Get just the folder for the currently specified application in BinaryPath
        /// </summary>
        private string BinaryDirectory
        {
            get
            {
                if (string.IsNullOrEmpty(BinaryPath))
                    return null;

                FileInfo file = new FileInfo(BinaryPath);
                if (!file.Exists)
                    return null;

                return file.DirectoryName;
            }
        }

        /// <summary>
        /// Gets just the name for the currently specified application in BinaryPath
        /// </summary>
        private string BinaryName
        {
            get
            {
                if (string.IsNullOrEmpty(BinaryPath))
                    return null;

                FileInfo file = new FileInfo(BinaryPath);
                if (!file.Exists)
                    return null;

                return file.Name;
            }
        }
        #endregion // private declarations

        #region public functions

        /// <summary>
        /// Run a non-threaded process and have the return be the exit code and response
        /// be included in a parameter
        /// </summary>
        /// <param name="response">parameter to pass the execution response</param>
        /// <returns>Exit value of the Binary executing</returns>
        public void Run()
        {
            if (string.IsNullOrEmpty(BinaryPath))
                throw new ArgumentNullException("BinaryPath");

            ProcessStartInfo startInfo = new ProcessStartInfo(BinaryPath, Arguments);
            startInfo.CreateNoWindow = true;
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardInput = true;
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;

            try
            {
                process = Process.Start(startInfo);

                //  wait for the process to return with an exit code (with a timeout variable)
                if (!process.WaitForExit(Config.ApplicationTimeout))
                    throw new TimeoutException("A time out event occurred while executing the process.");

                // set response info
                this.Response = (process.ExitCode == 0) ? process.StandardOutput.ReadToEnd() : process.StandardError.ReadToEnd();

                this.ExitCode = process.ExitCode;
            }
            catch (Exception ex)
            {
                // update response with exception message
                this.Response = ex.Message;
                this.ExitCode = -1;
                throw;
            }
            finally
            {
                if (null != process)
                    process.Dispose();
            }

        }

        /// <summary>
        /// Run a threaded process specified in the BinaryPath - 
        /// use for large streams of data to better handle the input buffering
        /// </summary>
        public void Run(Stream inputStream, Stream outputStream)
        {
            // verify input stream
            if (null == inputStream || !inputStream.CanRead)
                throw new ArgumentException("inputStream", "Stream is null or unreadable");

            // verify output stream
            if (null == outputStream || !outputStream.CanWrite)
                throw new ArgumentException("outputStream", "Stream is null or unwritable");

            // verify binary path
            if (string.IsNullOrEmpty(BinaryPath))
                throw new ArgumentNullException("BinaryPath");

            // temp stream to get command line output
            stdConsoleStream = new MemoryStream();

            // initialize process start info
            ProcessStartInfo startInfo = new ProcessStartInfo(BinaryPath, Arguments);
            startInfo.WorkingDirectory = this.WorkingDirectory;
            startInfo.CreateNoWindow = true;
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardInput = true;
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;

            try
            {
                //  start the gpg process and get back a process start info object
                process = Process.Start(startInfo);

                // check if app has already exited due to an error
                if (process.HasExited && process.ExitCode != 0)
                {
                    // load error line message into consoleStream
                    Utils.WriteTo(process.StandardError.BaseStream, stdConsoleStream);
                    this.ExitMessage = Utils.ConvertTo(stdConsoleStream);

                    this.ExitCode = process.ExitCode;
                    return;
                }

                // initialize stdOutputStream
                stdOutputStream = outputStream;
                stdConsoleStream = new MemoryStream();

                // prepare and start thread for standard output
                ThreadStart outputEntry = new ThreadStart(processThreadReader);
                Thread outputThread = new Thread(outputEntry);
                outputThread.Name = "outputthread";
                outputThread.Start();

                // prepare and start thread for error output
                ThreadStart consoleEntry = new ThreadStart(processThreadReader);
                Thread consoleThread = new Thread(consoleEntry);
                consoleThread.Name = "consolethread";
                consoleThread.Start();
                
                //  begin writing input stream to console
                Utils.WriteTo(inputStream, process.StandardInput.BaseStream);

                // commit buffer to console
                process.StandardInput.Flush();

                // close input stream
                process.StandardInput.Close();

                //  wait for the process to return with an exit code (with a timeout variable)
                if (!process.WaitForExit(Config.ApplicationTimeout))
                    throw new TimeoutException("Exceeded timeout threshold while executing process.");

                // wait for the output thread to join
                if (!outputThread.Join(Config.ApplicationTimeout / 2))
                    outputThread.Abort();

                // wait for the error thread to join
                if (!consoleThread.Join(Config.ApplicationTimeout / 2))
                    consoleThread.Abort();

                // check for error after waiting for process exit
                this.ExitCode = process.ExitCode;
                this.ExitMessage = Utils.ConvertTo(stdConsoleStream);
             }
            catch (Exception)
            { throw; }
            finally
            {
                if (null != process)
                    process.Dispose();
            }
        }
        #endregion // public functions

        #region private functions

        /// <summary>
        /// Reads the current threads and copies the output stream 
        /// </summary>
        private void processThreadReader()
        {
            // write output thread
            if (Thread.CurrentThread.Name.ToLower().Contains("outputthread"))
                Utils.WriteTo(process.StandardOutput.BaseStream, stdOutputStream);

            if (Thread.CurrentThread.Name.ToLower().Contains("consolethread"))
                Utils.WriteTo(process.StandardError.BaseStream, stdConsoleStream);
        }

        #endregion // private functions

        #region IDisposable Members
        /// <summary>
        /// Dispose of object
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Dispose of objects in class
        /// </summary>       
        protected virtual void Dispose(bool disposing)
        {
            if (!IsDisposed)
                if (disposing)
                {
                    this.WorkingDirectory = null;
                    this.BinaryPath = null;
                    this.Arguments = null;
                    this.ExitCode = -1;
                    this.ExitMessage = null;

                    if (null != Config)
                        Config.Dispose();

                    if (null != stdOutputStream)
                        stdOutputStream.Dispose();

                    if (null != stdConsoleStream)
                        stdConsoleStream.Dispose();

                    if (process != null)
                        process.Dispose();

                    // mark disposed
                    IsDisposed = true;
                }
        }

        /// <summary>
        /// Destructor method class
        /// </summary>
        ~App()
        {
            Dispose(false);
        }
        #endregion // IDisposable Members

    }
}
